Why isn't my AngularJS $scope.$watch being triggered on service changes? - javascript

In my controller, I have:
var timespanServiceFn;
timespanServiceFn = function() {
return timespanService.getTimespan();
};
$scope.$watch(timespanServiceFn, $scope.updateVolume());
My $scope.updateVolume() function just has a console.log so that I know I got there.
My timespanService is also super simple:
myApp.service('timespanService', [
'$http', '$q', function($http, $q) {
var currentTimespan;
currentTimespan = '3M';
this.getTimespan = function() {
return currentTimespan;
};
this.setTimespan = function(timespan) {
console.log('setting timespan to', timespan);
return currentTimespan = timespan;
};
}
]);
So why is it that when I changed the timespan, the $watch doesn't get triggered?

The watch takes two functions as arguments. You must pass the second parameter as argument but you just called it there:
$scope.$watch(timespanServiceFn, $scope.updateVolume());
so the return statement of $scope.updateVolume() will be passed to $watch function instead of the function definition itself.
Just change it to:
$scope.$watch(timespanServiceFn, $scope.updateVolume);
See demo

Related

AngularJS : $timeout is not a function

I am trying to add a delay of 2000ms by using the timeout method but it is not working if I try to call it in the function(searchfunc) inside the cotroller. It gives Error: $timeout is not a function .
Controller code :
var angularjsapp = angular.module('graphApp', ['ngAnimate', 'ui.bootstrap','ui.grid']);
angularjsapp.controller('AccordionDemoCtrl', function($scope, $timeout) {
$scope.searchfunc = function(search_name,$timeout) {
WebSocketTest(search_name,keyword_type);
//$scope.loading = false;
$timeout(function () {
$scope.loading = false;
}, 2000);
});
You're passing $timeout as a parameter to your function
$scope.searchfunc = function(search_name,*$timeout*
which will make it undefined when you call the function because you don't pass it along. Removing it will fix the issue.
scope.searchfunc = function(search_name)
You can read about how this works (closures) here
You override $timeout in the inner function.
Just omit it:
$scope.searchfunc = function(search_name) {
May be you mean second parameter to be keyword_type?
Because now it's looks like undefined.
$scope.searchfunc = function(search_name, keyword_type) {

How do I add result to my scope ng-click?

This is a relatively simple piece of code that calls a service and returns some data. I need to set the $scope with the result of the data. Is there an easy way to set this data to the scope without resorting to to binding the scope to the function in the then clause?
Angular Code
(function () {
var app = angular.module('reports', []);
var reportService = function($http, $q) {
var service = {};
service.getMenuData = function() {
var deffered = $q.defer();
$http.get('/Report/MenuData').success(function(data) {
deffered.resolve(data);
}).error(function(data) {
deferred.reject("Error getting data");
});
return deffered.promise;
}
return service;
};
reportService.$inject = ['$http', '$q'];
app.factory('reportService', reportService);
var reportMenuController =
function ($scope, $http, reportService) {
$scope.getMenuData = function(e) {
reportService.getMenuData().then(function(data) {
// Need to set the $scope in here
// However, the '$scope' is out of scope
});
}
};
reportMenuController.$inject = ['$scope', '$http', 'reportService'];
app.controller('ReportMenuController', reportMenuController);
})();
Markup
<div>
<div ng-controller="ReportMenuController">
<button ng-click="getMenuData()">Load Data</button>
</div>
</div>
There is absolutely no problem to set the $scope from within the function passed to then(). The variable is available from the enclosing scope and you can set your menu data to one of its fields.
By the way: You should consider to use then() instead of success() for your http request. The code looks much nicer because then() returns a promise:
service.getMenuData = function() {
return $http.get('/Report/MenuData').then(function(response) {
return response.data;
}, function(response) {
deferred.reject("Error getting data");
});
}
success() is deprecated by now.
I didn't notice the small detail missing in the plunker where my code was different.
(function () {
...
var reportMenuController =
function ($scope, $http, reportService) {
$scope.getMenuData = getMenuData;
function getMenuData(e) {
reportService.getMenuData().then(function(data) {
// Now I have access to $scope
});
}
};
...
})();
Notice the changes to the two lines as below:
$scope.getMenuData = getMenuData;
function getMenuData(e) {
This also begs a small question which is, "Why is it okay to set getMenuData to the $scope before it is declared?

Cannot get Karma/Jasmine to work with my angular controller

Ok. I have spent several hours trying in vain to get Karma to work with my Angular controller. Whatever I do, I get the following error. It seems that even if I remove the expectGET() calls, I still get the error; as soon as I call $http.flush();
TypeError: Cannot set property 'totalBeforeDiscounts' of undefined
The code for my controller is as follows:
var quotePadControllers = angular.module('quotePadControllers', []);
quotePadControllers.controller('QuotesController', ['$scope', '$http', '$q', function($scope, $http, $q){
var blankAddon;
// Setup initial state and default values
var ajaxGetAddOns = $http.get('/?ajax=dbase&where=aons'),
ajaxGetFrames = $http.get('/?ajax=dbase&where=fcats');
$q.all([ajaxGetAddOns, ajaxGetFrames]).then(function(results){
$scope.addons = results[0].data;
$scope.frames = results[1].data;
$scope.pairs = [
{
"frames" : angular.copy($scope.frames),
"addons" : angular.copy($scope.addons),
}
];
});
// Function for the 'add pair' button
$scope.addPair = function()
{
$scope.pairs.push({
"frames" : angular.copy($scope.frames),
"addons" : angular.copy($scope.addons)
});
};
// Function for the 'remove pair' button
$scope.removePair = function()
{
if ( $scope.pairs.length > 1 )
{
$scope.pairs.pop();
}
};
// Continually update the subtotal and total
$scope.$watch('pairs', function(pairs) {
var totalBeforeDiscounts = 0;
angular.forEach(pairs, function(pair) {
var subTotal = 0;
angular.forEach(pair.addons, function(addon) {
subTotal += addon.added ? addon.price : 0;
});
subTotal += pair.currentFrame !== undefined ? pair.currentFrame.price : 0;
pair.subTotal = subTotal;
totalBeforeDiscounts += subTotal;
});
pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);
}]);
and my test code:
describe('QuotesController', function()
{
beforeEach(module('quotePadApp'));
var ctrl, $scope, $http, frameCatsHandler, addOnsHandler, createController;
// Setup tests
beforeEach(inject(function($controller, $rootScope, $httpBackend, _$q_) {
$scope = $rootScope.$new();
$http = $httpBackend;
frameCatsResponse = [{"id":145,"price":25,"brand":"mybrand"},
{"id":147,"price":45,"brand":"mybrand"},
{"id":148,"price":69,"brand":"mybrand"}];
addOnsHandler = [{"id":1,"name":"addon1","price":30,"includeIn241":0,"description":null},
{"id":2,"name":"addon2","price":60,"includeIn241":0,"description":null}];
frameCatsHandler = $http.when('GET', '/?ajax=dbase&where=fcats').respond(frameCatsResponse);
addOnsHandler = $http.when('GET', '/?ajax=dbase&where=aons').respond(addOnsHandler);
createController = function()
{
return $controller('QuotesController', {'$scope' : $scope });
};
}));
it('Should request frame cats and addons from the database', function()
{
$http.expectGET('/?ajax=dbase&where=aons');
$http.expectGET('/?ajax=dbase&where=fcats');
createController();
$http.flush();
});
});
This is because you have the following watch statement in your controller trying to set a totalBeforeDiscounts property on $scope.pairs.
$scope.$watch('pairs', function(pairs) {
// ...
pairs.totalBeforeDiscounts = totalBeforeDiscounts;
}, true);
In your tests, when you create the controller and then call $http.flush(), that's actually triggering a $digest cycle. This kicks off all watchers.
createController();
$http.flush();
The watch handler above will execute and since it executes before $scope.pairs has any value, the pairs argument passed into the watch handler is undefined, resulting in your error.
As per the documentation:
After a watcher is registered with the scope, the listener fn is
called asynchronously (via $evalAsync) to initialize the watcher. In
rare cases, this is undesirable because the listener is called when
the result of watchExpression didn't change. To detect this scenario
within the listener fn, you can compare the newVal and oldVal. If
these two values are identical (===) then the listener was called due
to initialization.
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch
Also, in the rest of your code you have $scope.pairs as an array, but in the watch you are trying to set a property like totalBeforeDiscounts on it. This doesn't look right.

Injecting a $scope.property by ref into a factory instance

I need a Factory object that can be applied with a $scope.property to get a result. I also need to know if either the modifier in the factory instance, or the $scope.property changes to update the result. How I am seeing this pattern might be wrong of course.
app.factory('MyFactory',
function () {
var MyFactory = function (name, modifier) {
this.name = name;
this.result = 0;
this.modifier = modifier;
}
//I might want to call this when modifier or MyProperty changes
MyFactory.prototype.modifyingMethod = function () {
this.result = this.modifier * //externalProperty;
}
MyFactory.prototype.watcher = function () {
//no idea what I will do here or if I need this at all
// I need to recalculate the result like with $watch
this.modifyingMethod();
}
return MyFactory;
}
)
app.controller('MyCtrl'
function($scope, MyFactory) {
$scope.MyProperty = 42;
$scope.MyFactoryOptions = [
new MyFactory('Option1', 1),
new MyFactory('Option2', 2),
new MyFactory('Option3', 3),
new MyFactory('Option4', 4),
new MyFactory('Option5', 5)
];
}
So I have the problem that I need to $watch MyProperty and the modifier (it can be changed bu users) so I can change the result. If the Property is a value type passing it into the Factory constructor will not work. Perhaps I could pass a function in the returns MyProperty.
Can I set up internal $watch on the factory. If I would do this outside of the factory, in the controller, I would need to do it for each instance. Should I perhaps set up some sort of register method on my factory object?
Are there any suggestions, patterns or something I might want to use?
Basically you could understand your Factory as an interface to a collection of objects (either an array or associative array respectively pure javascript object).
I find your approach with Objects very appealing and I am sure you can achieve similar things. Still I put together a fiddle, that shows how I would solve the problem:
In a MVC pattern your Factory would be the model, the controller should be as simple as possible and your HTML with directives represents the view.
You controller watches for changes from the user ($scope.MyProperty with $watch). While the model is self aware of any depending external property changes. Note that the changes of the ExternalObject service/factory will only be recognizable, if those aren't primitive values. That is why in the ExternalObject factory I return the whole object.
In a perfect world you wouldn't need to listen for changes with an interval, but will receive a javascript event. If this is possible, do it! Note that object updates out of Angular's scope will need you to do a $scope.$apply(), if you want to see the changes in the view. $intervaland $timeout both call a $scope.apply(), so using them is best practice.
Actually there still has to be a lot of cleanup to be done in this code, but it might give you a basic idea how to use an alternative structure:
var app = angular.module('yourApp', []);
window.yourGlobalObject = {};
setInterval(function () {
yourGlobalObject.externalProperty = Math.floor(Math.random() * 5000);
}, 1000);
app.factory('ExternalObject', ['$window', function ($window) {
return $window.yourGlobalObject;
}]);
app.factory('MyFactory', ['$interval', 'ExternalObject', function ($interval, ExternalObject) {
var options = [{
name: 'Option1',
modifier: 1
}, {
name: 'Option2',
modifier: 2
}, {
name: 'Option3',
modifier: 3
}],
cachedExternalProperty = 0,
cachedMyProperty = 0;
MyFactory = {
getOptions: function () {
return options;
},
addOption: function (name, modifier) {
options.push({
name: name,
modifier: modifier
});
},
setMyProperty: function (value) {
cachedMyProperty = value;
},
setResults: function (myProperty) {
angular.forEach(options, function (option, key) {
option.result = option.modifier * ExternalObject.externalProperty * myProperty;
});
console.log(options);
}
};
// let the service check for updates in the external property, if changed
$interval(function () {
if (cachedExternalProperty !== ExternalObject.externalProperty) {
cachedExternalProperty = ExternalObject.externalProperty;
MyFactory.setResults(cachedMyProperty);
}
}, 1000);
return MyFactory;
}]);
app.controller('MyCtrl', ['$scope', 'MyFactory', function ($scope, MyFactory) {
$scope.MyProperty = 42;
$scope.MyFactoryOptions = MyFactory.getOptions();
$scope.setResults = function () {
MyFactory.setResults($scope.MyProperty);
};
$scope.$watch('MyProperty', function (value) {
MyFactory.setMyProperty(value)
MyFactory.setResults(value);
});
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body>
<section ng-app="yourApp" ng-controller="MyCtrl">
<button ng-click="setResults(MyProperty)">Update Results</button>
<div ng-repeat="factory in MyFactoryOptions">{{factory.name}} {{factory.result}}</div>
<input type="number" ng-model="MyProperty">
</section>
</body>

How to set a variable in different controller in AngularJS?

I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.

Categories

Resources