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.
Related
In the following code, am returning calling a server method "getUserNames()" that returns a JSON and assigning it to main.teamMembers variable. There is a viewAll button that is part of a report i am building which calls the method "$scope.viewAllTeamMembers = function($event)" listed below.
Now, the view all button lists all the value stored in main.teamMembers is not working on the first time the report is loaded. But when i navigate to other buttons in the report and try to access the viewAll its working.
The reason i asked about returning a JSON from an anonymous was because, i found that when its a global variable the viewAll button works the first time. Please help me understand with what i am missing.
angular.module('kamApp')
.controller('MainCtrl', [
'$q',
'$rootScope',
'$scope',
'$timeout',
'endpoints',
'kamService',
'queries',
'translations',
function($q, $rootScope, $scope, $timeout, endpoints, kamService, queries, translations) {
var getUserNamesRequest = endpoints.getUserNames($rootScope.teamMemberIds).then(function(userDdata){
return userDdata;
});
getUserNamesRequest.then(function(userDdata,$scope) {
$rootScope.userNameList = kamService.extractUserNameList(userDdata);
main.teamMembers=kamService.concatTeamMemberName(
main.teamMembersData,
$rootScope.userNameList.list
);
main.teamMembers.list = kamService.sortList(main.teamMembers.list, 'role', 'name');
});
}]);
--Directive
angular.module('kamApp')
.directive('teamMember', function() {
return {
templateUrl: 'views/team-member.html',
replace: true,
restrict: 'E',
scope: {
teamMembers: '=',
viewSwitch: '=',
changeReportTitle: '&'
},
link: function($scope) {
$scope.itemLimit = 4;
$scope.isOddLength = $scope.teamMembers.list.length % 2 !== 0;
$scope.viewAllTeamMembers = function($event) {
$event.target.style.opacity = 0.6;
$scope.viewSwitch.dashboard = false;
$scope.viewSwitch.teamMember = true;
$scope.changeReportTitle()($scope.teamMembers.objectName.plural);
};
}
};
});
--HTML Code
"<div class=\"expand-link inline-block-div\" ng-click=\"viewAllTeamMembers($event)\"> \n"+
In example 1, you're setting usernames to the promise returned by getUserNames().then() rather than the returned value and JSON.stringify(userNames); is run before the AJAX request completes.
In example 2, again, JSON.stringify(userNames); is being run before the AJAX request completes.
Instead you should put the code that is dependent on result inside the then callback.
var result={};
getUserNames($rootScope.teamMemberIds).then(function(userDdata){
result = userDdata;
// Code dependent on result
});
If you want to run other code that is dependent on the the variable being set that
var result={};
var getUserNamesRequest = getUserNames($rootScope.teamMemberIds).then(function(userData){
result = userData;
// Code dependent on result
// return the data so that chained promises get the original data.
return userData;
});
// Other code dependent on `result` being set.
getUserNamesRequest.then((userData) => {
// use either `result` or `userData` here.
});
I need to pass a scope value to a service, what I'm doing now is declearing the service's function in the controller and passing the scope value as a parameter to this function. Here is the code
HTML code
<md-datepicker ng-model="dateDebut" md-placeholder="Enter date"> </md-datepicker>
<md-datepicker ng-model="dateFin" md-placeholder="Enter date" ></md-datepicker>
Controller code
app.controller('graphCtrl', function($scope, $stateParams, graphDetails, $filter,$http) {
var self = this;
self.dateDebut = $scope.dateDebut;
self.dateFin = $scope.dateFin;
var mettreAJour = graphDetails.mettreAJour(self.dateDebut, self.dateFin);
$scope.labels = mettreAJour.labels;
$scope.data = mettreAJour.data;
});
Service code
app.factory('graphDetails', function($filter, $http) {
var labels = [];
var data = [
[]
];
return {
mettreAJour: function(dateDebut, dateFin) {
if (dateDebut && dateFin) {
var dd = $filter('date')(dateDebut, 'yyyy-MM-dd;HH:mm:ss');
var df = $filter('date')(dateFin, 'yyyy-MM-dd;HH:mm:ss');
var dif = dateFin.getDate() - dateDebut.getDate();
//do somthing with labels and data
return {
labels : labels,
data : data
};
}
}
};
});
So I get as an error labels is not defined, and if I comment it I get this error:
Cannot read property 'getDate' of undefined
which means the code does not recognize dateFin nor dateDebut.
Is there another way to pass the scope to the service, or am I missing something in my current code?
If I understand the question correctly you need to somehow reevalute values when either dateDebut or dateFin properties of scope change.
To achive this you can use $watch or $watchGroup methods of the scope. Simplified Demo.
For example
app.controller('graphCtrl', function($scope, graphDetails) {
// start watcher
$scope.$watchGroup(['dateDebut', 'dateFin'], function(args) {
var debut = args[0], fin = args[1];
angular.extend($scope, graphDetails.mettreAJour(debut, fin))
})
})
Most probably this is because you are trying to return labels from mettreAJour but it is not aware of labels & data .
Hope this below snippet will be useful.
app.factory('graphDetails', function($filter, $http) {
var _graphDeatilsObject = {};
_graphDeatilsObject.labels = [];
_graphDeatilsObject.data = [
[]
];
_graphDeatilsObject.mettreAJour = function(dateDebut, dateFin) {
// Rest of the code
}
return _graphDeatilsObject;
});
Also take a look at inline array annotation which is required if you are minifying the code
I would like to do something like this:
app.config(function($routeProvider){
$routeProvider.when('products/list', {
controller: 'ProductListCtrl',
templateUrl : 'products/list/view.html',
resolve : { data : function(){
...
},
loadingTemplateUrl : 'general/loader.html'
}
});
I would like to have the loading page in a different view.
This would make the code in the view and controller of every page cleaner, (no <...ng-include ng-show="loading"...>). This would also mean that I don't have to $scope.$watch the data for changes. Is there a clean solution to do something similar (not necessarily in the .config method) or an alternative library to do this?
Assuming you want to show some general template for all state transitions while the data is resolved, my suggestion is to listen to the events fired by the routing library. This allows to use one central point to handle all state transitions instead of polluting the routing config (which I think will not be that easy to do).
Please see the docs for $routeChangeStart, $routeChangeSuccess and of course $routeChangeError at the angular router docs
Maybe someone could be interested in what I did: I created a new service and a new view directive. It could seem like a lot of work, but doing this was much easier than I had expected. The new service enables me to separate the main view from the loading view, that I could reuse in all pages of the application. I also provided the possibility to configure an error template url and error controller, for when the loading failed.
The Angular $injector, $templateRequest and $controller services do most of the work. I just had to connect a directive, that depends on these services, to the right event ($locationChangeSuccess), and to the promise, retrieved (using $q.all) from the resolve object's functions. This connection was done in the route service. The service selects the right template url and comtroller, and passes it on for the directive to handle.
A shortened version (with the getCurrentConfig method left out):
RouteService:
(function () {
'use strict';
// provider:
angular.module('pikcachu')
.provider('pikaRouteService', [function () {
var routeConfigArray;
var otherwiseRouteConfig;
//configuration methods
this.when = function (url, routeConfig){
routeConfigArray.push({url: url, routeConfig: routeConfig});
return this;
}
this.otherwise = function(routeConfig){
otherwiseRouteConfig = routeConfig;
return this;
}
// service factory:
this.$get = ['$rootScope', '$location', '$q', '$injector', '$templateRequest',
function ($rootScope, $location, $q, $injector, $templateRequest) {
function RouteService() {
this.setViewDirectiveUpdateFn = function(){ /*...*/ }
function init(){
$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess);
}
function onLocationChangeSuccess(){
// get the configuration based on the current url
// getCurrentConfig is a long function, because it involves parsing the templateUrl string parameters, so it's left out for brevity
var currentConfig = getCurrentConfig($location.url());
if(currentConfig.resolve !== undefined){
// update view directive to display loading view
viewDirectiveUpdateFn(currentConfig.loadingTemplateUrl, currentConfig.loadingController);
// resolve
var promises = [];
var resolveKeys = [];
for(var resolveKey in currentConfig.resolve){
resolveKeys.push(resolveKey);
promises.push($injector.invoke(resolve[resolveKey]));
}
$q.all(promises).then(resolveSuccess, resolveError);
function resolveSucces(resolutionArray){
// put resolve results in an object
var resolutionObject = {};
for(var i = 0; i< promises.length;++i){
resolved[resolveKeys[i]] = resolutionArray[i];
}
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
function resolveError(){
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
}
}
init();
}
return new RouteService();
}]
})();
View directive
(function () {
'use strict';
angular.module('pikachu')
.directive('pikaView', ['$templateRequest', '$compile', '$controller', 'pikaRouteService', function ($templateRequest, $compile, $controller, pikaRouteService) {
return function (scope, jQdirective, attrs) {
var viewScope;
function init() {
pikaRouteService.listen(updateView);
}
function updateView(templateUrl, controllerName, resolved) {
if(viewScope!== undefined){
viewScope.$destroy();
}
viewScope = scope.$new();
viewScope.resolved = resolved;
var controller = $controller(controllerName, { $scope: viewScope });
$templateRequest(templateUrl).then(onTemplateLoaded);
function onTemplateLoaded(template, newScope) {
jQdirective.empty();
var compiledTemplate = $compile(template)(newScope);
jQdirective.append(compiledTemplate);
}
}
init();
};
}
]);
})();
Hitting the ceiling of my Angular knowledge and I have been going around in circles on this.
Basically I have video player and chapter list directives, each with a controller. The controllers use the same model service which looks like this:
.service('VideoPlayerModel', function(){
var model = this;
model.chapters = {
chapterPos: 0,
targetChapter:null,
data: []
},
model.getVideoData = function() {
return model.videoData;
};
model.setVideoData = function(vData){
...
...
...
};
});
In the video player controller as the time of the player updates it finds the needed chapter data and updates the model.chapters data like this:
updateChapter: function(currentTime){
var chapters = VideoPlayerModel.chapters;
var chaptersCtrl = videoPlayerCtrl.chapters;
if (chapters.nextChapter.start <= currentTime) {
chapters.chapterPos = chapters.chapterPos + 1;
chaptersCtrl.setChapter(); //This finds and sets the Target Chapter
}
},
After setChapter runs I call console.log(VideoPlayerModel.chapters) and I can see the data model has updated with a result like this:
Object {chapterPos: 1, targetChapter: Object, data: Array[6], nextChapter: Object}
However the watch in the ChapterListCtrl doesn't fire and any of the onscreen items displaying the ChapterPos still show just the initial val of 0.
The controller looks like this:
.controller("ChapterListCtrl", ['$scope', 'VideoPlayerModel', function($scope, VideoPlayerModel) {
$scope.chapters = VideoPlayerModel.chapters;
$scope.$watch(function() { return VideoPlayerModel.chapters; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
}])
I have tried different ways and ended up with this, not sure if I am in the complete wrong direction now. Can anyone please help?
You don't need to use $watch, $broadcast or $on. This is best solved by regular JavaScript thinking.
Your problem is $scope.chapters = newValue; That is where you break the binding that your controllers use by introducing a new object unrelated to your service.
What you should to instead is to think about your service model.chapters = {..} and say hey! This is THE ONE object that I will use. And if I want to change the data in this object anywhere, I will switch the data inside the object and NOT assign a new object to the reference I use.
To do this I use the following methods:
transferDataList = function (from, to) {
/*
http://stackoverflow.com/questions/1232040/empty-an-array-in-javascript
*/
to.length = 0;
for (var i = 0; i < from.length; i++) { to.push(from[i]); }
};
transferDataMap = function (from, to) {
/*
http://stackoverflow.com/questions/684575/how-to-quickly-clear-a-javascript-object
*/
var member;
for (member in to) { delete to[member]; }
for (member in from) { to[member] = from[member]; }
};
And when I want to change the data in my object I DON'T do:
$scope.chapters = newValue;
Instead I do:
transferDataMap(newValue, $scope.chapters);
Or:
transferDataList(newValue, $scope.chapters);
This way you will keep your binding and your Angular interfaces will always be updated.
You can use $broadcast() and $on() function to achieve your requirement.
$broadcast() will flush an event to all it's child controller. So, you can $broadcast() an event with your new value to all controllers when you set a new value to your shared model.
Add a broadcast method in your shared service.
model.setVideoData = function(vData){
UpdateYourModel();
// Inform that your model is updated
$rootScope.$broadcast('modelUpdated');
}
And now you can add a listener for the event modelUpdated in all your controllers.
$scope.$on('modelUpdated', function () {
$scope.controllerModel = VideoPlayerModel.getVideoData(); // Update your controller model
}
And also, inject $rootScope to your service,
.service("VideoPlayerModel", ["$rootScope", function($rootScope){
// define your service here.
}] );
That's all !!!
I hope this will help you.
Try changing your watcher to:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
Alternatively if that doesn't achieve what you want, you can enable a deep watch by passing the third argument:
$scope.$watch('chapters', function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
}, true);
Your watcher doesn't fire because it always returns the same chapters which Angular considers as not-changed because it checks by reference. Your watcher can also be refactored as:
$scope.$watch(function() { return VideoPlayerModel.chapters.length; }, function(newValue, oldValue){
$scope.chapters = newValue;
console.log("A Change"); // Only runs at initialisation.
});
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