How to apply watcher on a variable without using $scope? - angularjs

Is there any way to watch changed value of a variable without using $scope?. My manager told me that we may migrate our code to angular2 which do not have $scope so we have to write the code in the way so that when we migrate it to angular2 it needs minimal changes. So now i want to keep track changes of a variable which we do in angular like :
$scope.$watch('myVar', function() {
alert('hey, myVar has changed!');
});
Now i want this to something like
app.controller('auditCtrl',
function($scope, $localStorage) {
var adc = this;
angular.extend(this, {
$state: $state,
count: 0
}
//What i want is something like
abc.$watch('count', function() {
alert('hey, count has changed!');
});
})

No, thats not possible. But ask yourself, what is the variable you want to $watch for? If it is a form field, you can simply use ng-change.
If you can use es2015 Syntax, you can add Getters and Setters on the Controllers Prototype which then triggers an update Function.
Something like this:
let auditCtrl = function () {
const adc = self;
adc.counterItem = null;
// etc.
};
Object.defineProperty(auditCtrl.prototype,
"counterItem", {
get: function () {
return this.counterItem;
},
set: function (newValue) {
this.counterItem = newValue;
// Call method on update
this.onCounterItemChange(this.counterItem);
},
enumerable: true,
configurable: true
});
Or, in my opinion the best approach, use a component architecture for your application. With components, you can use some built-in lifecycle-hooks like $onInit, $onChanges etc. This way, you are also thinking in the angular2 way, since you might want to migrate.

Related

dependency injection into decorator functions in angularJS

When using angularJS you can register a decorating function for a service by using the $provide.decorator('thatService',decoratorFn).
Upon creating the service instance the $injector will pass it (the service instance) to the registered decorating function and will use the function's result as the decorated service.
Now suppose that thatService uses thatOtherService which it has injected into it.
How I can I get a reference to thatOtherService so that I will be able to use it in .myNewMethodForThatService() that my decoratorFN wants to add to thatService?
It depends on the exact usecase - more info is needed for a definitive answer.
(Unless I've misunderstood the requirements) here are two alternatives:
1) Expose ThatOtherService from ThatService:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this._somethingElseDoer = ThatOtherService;
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-1]: Let\'s try something new...');
// We still need to do something else afterwards, so let's use
// `ThatService`'s dependency (which is exposed as `_somethingElseDoer`)
$delegate._somethingElseDoer.doSomethingElse();
};
return $delegate;
});
});
2) Inject ThatOtherService in the decorator function:
.service('ThatService', function ThatServiceService($log, ThatOtherService) {
this.doSomething = function doSomething() {
$log.log('[SERVICE-1]: Doing something first...');
ThatOtherService.doSomethingElse();
};
})
.config(function configProvide($provide) {
$provide.decorator('ThatService', function decorateThatService($delegate, $log, ThatOtherService) {
// Let's add a new method to `ThatService`
$delegate.doSomethingNew = function doSomethingNew() {
$log.log('[SERVICE-2]: Let\'s try something new...');
// We still need to do something else afterwatds, so let's use
// the injected `ThatOtherService`
ThatOtherService.doSomethingElse();
};
return $delegate;
});
});
You can see both approaches in action in this demo.

How to create any kind of immutable providers in AngularJS

Consider the following example:
angular.module('demo')
.service('MyService', function () {
this.fn = function () {
console.log('MyService:fn');
};
})
.factory('MyFactory', function () {
function fn() {
console.log('MyFactory:fn');
}
return { fn: fn };
})
.value('MyValue', {
fn: function () {
console.log('MyValue:fn');
}
})
.constant('MyConstant', {
fn: function () {
console.log('MyConstant:fn');
}
})
.run(function (MyService, MyFactory, MyValue, MyConstant) {
MyService.fn();
MyFactory.fn();
MyValue.fn();
MyConstant.fn();
MyService.fn = undefined;
MyFactory.fn = undefined;
MyValue.fn = undefined;
MyConstant.fn = undefined;
})
.run(function (MyService, MyFactory, MyValue, MyConstant) {
MyService.fn();
MyFactory.fn();
MyValue.fn();
MyConstant.fn();
});
When the first run() is executed, all 4 console logs will be executed and print something on the console. Then I set each of the providers fn function to undefined for simplification purposes, say someone rewrote this function somewhere (which is something I want to prevent).
On the second run() block, everything is undefined and errors will be thrown. I'm confused by this... Shouldn't at least some of them (constant is the first to come to mind) be immutable objects?
Is this the expected behavior or am I doing something wrong?
Why is this a surprise. Angular is a framework running on top of Javascript, and Javascript is a dynamic language. Your question is really about the language construct.
First, recognize that all the calls are, at the end of the day, registering a provider that would return an object to be injected. .service, .factory, and .value are just short hands for .provider (.constant is a bit different).
Having established that there is no difference between them once the object is injected, all you then need to concern yourself with is how to make that object immutable.
Javascript provides Object.freeze function, so for example, you could do:
var immutable = {
fn: function () {
console.log('MyConstant:fn');
}
};
Object.freeze(immutable);
app.constant("MyConstant", immutable);
The functions constant, factory, service etc. allow you to create Javascript objects that are created once, then cached by angular, and injected into components as/when needed. Because these are Javascript objects, then (ignoring the possibility of using freeze) any bit of code has access to these objects can modify properties on them, as you have demonstrated.
Although properties on the objects themselves can be modified, the object itself can't be changed to another one, so if you really want an immutable provider, that is safe from all tampering, one way that I can think of is to use a factory that returns not an object, but a getter function:
app.factory('MyFactory', function() {
var functions = {
myFunctionName: function() {
}
};
return function(functionName) {
return functions[functionName];
};
});
which can be used, say in a controller, as
app.controller('MyController', function(MyFactory) {
MyFactory('myFunctionName')();
});
A drawback of using this over the more traditional way of exposing all the methods and allowing the possibility of the object to be modified is that I suspect unit testing could be more complicated: you wouldn't be able to easily create spies on your factory methods by using createSpy(MyFactory, 'myFunctionName').

AngularJS, Using a Service to call into a Controller

I am new to Angular, what I would like to accomplish is: From a Service / Factory to call methods directly into a controller.
In the following code, I would like from the valueUserController I would like to create a method from the service myApi and set the value inside the valueController.
Here is my code:
modules/myApi.js
var MyApi = app.factory('MyApi', function()
var api = {};
api.getCurrentValue = function() {
// needs to access the Value controller and return the current value
}
api.setCurrentValue = function(value) {
// needs to access the Value controller and set current value
}
api.getValueChangeHistory = function() {
// access value controller and return all the values
}
);
controllers/value.js
app.controller('valueController', function($scope) {
var value = 0;
function getValue() {
return value;
}
function setValue(inValue) {
value = inValue;
}
// ......
});
controllers/valueUser.js
app.controller('valueUserController', function($scope, myApi) {
function doStuff() {
var value = myApi.getValue();
value++;
myApi.setValue(value);
}
});
I am finding to do this in AngularJS pretty difficult and I haven't found any similar post on here.
Thanks for any help,
Andrea
Trying to communicate with a specific controller from a service is not the correct way of thinking. A service needs to be an isolated entity (which usually holds some state), by which controllers are able to interact with.
With this in mind, you can use something like an event pattern to achieve what you are looking for. For example, when your service completes some particular process, you can fire an event like so:
$rootScope.$broadcast('myEvent', { myValue: 'someValue' });
Then any controller in your system could watch for that event and perform a specific task when required. For example, inside your controller you could do the following:
$scope.$on('myEvent', function(event, data){
// Do something here with your value when your service triggers the event
console.log(data.myValue);
});

How to access/update $rootScope from outside Angular

My application initializes an object graph in $rootScope, like this ...
var myApp = angular.module('myApp', []);
myApp.run(function ($rootScope) {
$rootScope.myObject = { value: 1 };
});
... and then consumes data from that object graph (1-way binding only), like this ...
<p>The value is: {{myObject.value}}</p>
This works fine, but if I subsequently (after page rendering has completed) try to update the $rootScope and replace the original object with a new one, it is ignored. I initially assumed that this was because AngularJS keeps a reference to the original object, even though I have replaced it.
However, if I wrap the the consuming HTML in a controller, I am able to repeatedly update its scope in the intended manner and the modifications are correctly reflected in the page.
myApp.controller('MyController', function ($scope, $timeout) {
$scope.myObject = { value: 3 };
$timeout(function() {
$scope.myObject = { value: 4 };
$timeout(function () {
$scope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Is there any way to accomplish this via the $rootScope, or can it only be done inside a controller? Also, is there a more recommended pattern for implementing such operations? Specifically, I need a way to replace complete object graphs that are consumed by AngularJS from outside of AngularJS code.
Thanks, in advance, for your suggestions,
Tim
Edit: As suggested in comments, I have tried executing the change inside $apply, but it doesn't help:
setTimeout(function() {
var injector = angular.injector(["ng", "myApp"]);
var rootScope = injector.get("$rootScope");
rootScope.$apply(function () {
rootScope.myObject = { value: 6 };
});
console.log("rootScope updated");
}, 5000);
Except for very, very rare cases or debugging purposes, doing this is just BAD practice (or an indication of BAD application design)!
For the very, very rare cases (or debugging), you can do it like this:
Access an element that you know is part of the app and wrap it as a jqLite/jQuery element.
Get the element's Scope and then the $rootScope by accessing .scope().$root. (There are other ways as well.)
Do whatever you do, but wrap it in $rootScope.$apply(), so Angular will know something is going on and do its magic.
E.g.:
function badPractice() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.scope().$root; // 2
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice :(';
});
}
See, also, this short demo.
EDIT
Angular 1.3.x introduced an option to disable debug-info from being attached to DOM elements (including the scope): $compileProvider.debugInfoEnabled()
It is advisable to disable debug-info in production (for performance's sake), which means that the above method would not work any more.
If you just want to debug a live (production) instance, you can call angular.reloadWithDebugInfo(), which will reload the page with debug-info enabled.
Alternatively, you can go with Plan B (accessing the $rootScope through an element's injector):
function badPracticePlanB() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.injector().get('$rootScope'); // 2b
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice too :(';
});
}
After you update the $rootScope call $rootScope.$apply() to update the bindings.
Think of modifying the scopes as an atomic operation and $apply() commits those changes.
If you want to update root scope's object, inject $rootScope into your controller:
myApp.controller('MyController', function ($scope, $timeout, $rootScope) {
$rootScope.myObject = { value: 3 };
$timeout(function() {
$rootScope.myObject = { value: 4 };
$timeout(function () {
$rootScope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Demo fiddle

Update scope value when service data is changed

I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>

Resources