AngularJS Jasmine Tests - javascript

I've been learning AngularJS for a while now and am finally getting my head around how it works after being a back-end developer for years.
However, I'm having an enormous amount of trouble understanding how unit testing works with Karma + Jasmine.
Every article I read either stops at testing a controller $scope variable for a value or dives so far into the deep end I get lost in the first paragraph.
I'm hoping someone can write a demo test for this controller so I can get my head around how to test controller functions with private variables etc.
financeApp.controller('navController', ['$scope', '$resource', '$cookies', '$location', function ($scope, $resource, $cookies, $location) {
// Set default values
$scope.resultList = [];
$scope.cookieExp = moment().add(3, 'months').toDate();
$scope.dataLoaded = true;
$scope.codesList = [];
// Update watchlist item stock prices
$scope.updateWatchItem = function (items) {
sqlstring = items.join("\",\"");
var financeAPI = $resource('https://query.yahooapis.com/v1/public/yql', {callback: "JSON_CALLBACK" }, {get: {method: "JSONP"}});
financeAPI.get({q: decodeURIComponent('select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22' + sqlstring + '%22)'),
format: 'json', env: decodeURIComponent('store%3A%2F%2Fdatatables.org%2Falltableswithkeys')})
.$promise.then(function (response) {
var quotes = response.query.results.quote;
quotes = Array.isArray(quotes) ? quotes : [quotes];
quotes.forEach(function (quote) {
$scope.createWatchItem(quote);
});
}, function (error) {
alert("ERROR: There was an issue accessing the finance API service.");
});
};
// Add a new watchlist item (triggered on button click)
$scope.newWatchItem = function () {
var newcode = $scope.asxcodeinput;
if (newcode == null) {
alert('Please enter a valid ASX equities code...');
return;
}
else if ($scope.codesList.indexOf(newcode + '.AX') > -1) {
alert('You are already tracking ' + newcode.toUpperCase() + '!');
return;
}
$scope.dataLoaded = false;
var financeAPI = $resource('https://query.yahooapis.com/v1/public/yql', {callback: "JSON_CALLBACK" }, {get: {method: "JSONP"}});
financeAPI.get({q: decodeURIComponent('select%20*%20from%20yahoo.finance.quote%20where%20symbol%20in%20(%22' + newcode + '.AX%22)'),
format: 'json', env: decodeURIComponent('store%3A%2F%2Fdatatables.org%2Falltableswithkeys')})
.$promise.then(function (response) {
$scope.dataLoaded = true;
var quote = response.query.results.quote;
if(quote.StockExchange != null) {
$scope.createWatchItem(quote);
$cookies.putObject('codesCookie', $scope.codesList, {expires: $scope.cookieExp});
$location.path('/' + (quote.Symbol).split('.')[0].toUpperCase());
}
else {
alert("Woops! Looks like that stock doesn't exist :(");
}
}, function (error) {
alert("ERROR: There was an issue accessing the finance API service.");
});
$scope.asxcodeinput = "";
};
// Delete a watchlist item (triggered on delete icon click)
$scope.deleteWatchlistItem = function (asxcode) {
$scope.resultList.forEach(function (result, key) {
if(result.Symbol == asxcode) {
$scope.resultList.splice(key, 1);
}
});
$scope.codesList.forEach(function (code, key) {
if(code == asxcode) {
$scope.codesList.splice(key, 1);
}
});
$cookies.putObject('codesCookie', $scope.codesList, {expires: $scope.cookieExp});
$location.path('/');
};
// Add new watchlist item to lists of watched items
$scope.createWatchItem = function (quote) {
$scope.resultList.push(quote);
$scope.codesList.push(quote.Symbol);
};
// Get current page for navigation menu CSS
$scope.isActive = function (location) {
return location === $location.path();
};
// If the cookie is set and not empty, populate the watchlist items with the cookie contents
if($cookies.getObject('codesCookie') && $cookies.getObject('codesCookie').length > 0) {
$scope.updateWatchItem($cookies.getObject('codesCookie'));
}
}]);
Also, if anyone can recommend an easy to read article on unit testing in AngularJS I'd appreciate it.

That is a big lump to start testing with. I suggest looking at the tutorial page REST and Custom Services on the angular site and put the resource stuff in a service.
I suggest viewing some good videos on jasmine at https://www.youtube.com/channel/UC4Avh_hoUNIJ0WL2XpcLkog
I do recommend you view up to and including the one on spies.

Related

Sharing values between states in AngularJS

How can I share the variable "demo" between all of my states in angular? I've tried to do this by creating a 'mainController' and placing it inside of my states along with another controller in the 'view' like shown below. But I keep getting an error saying that 'demo' is not defined when I try console.log(demo); in my states.
My Main Controller
app.controller('mainController', function ($scope,$http,$state,$window,$timeout) {
var demo = "works";
})
How I've tried to intergrate that 'mainController' within my states alongside a controller in my 'view' (NOTE: I've closed the brackets on the example I'm showing you, there's just a lot of javascript below this script)
.state('checkIn',{
controller:'mainController',
url:'/checkIn',
views: {
'main':{
templateUrl: 'templates/checkIn.html',
controller: function($scope,$http,$state,$window,$timeout){
console.log(demo);
The rest of my javascript
// Ionic Starter App
// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
var app = angular.module('starter', ['ionic','ui.router','service'])
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs).
// The reason we default this to hidden is that native apps don't usually show an accessory bar, at
// least on iOS. It's a dead giveaway that an app is using a Web View. However, it's sometimes
// useful especially with forms, though we would prefer giving the user a little more room
// to interact with the app.
if (window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
cordova.plugins.Keyboard.disableScroll(true);
}
if (window.StatusBar) {
// Set the statusbar to use the default style, tweak this to
// remove the status bar on iOS or change it to use white instead of dark colors.
StatusBar.styleDefault();
}
});
});
app.controller('mainController', function ($scope,$http,$state,$window,$timeout) {
var demo = "works";
})
app.config(function($stateProvider,$urlRouterProvider,$httpProvider){
$httpProvider.interceptors.push('AuthInter');
$urlRouterProvider.otherwise('/home')
$stateProvider
.state('home',{
controller:'mainController',
url:'/home',
views: {
'main':{
templateUrl: 'templates/home.html',
}
}
})
.state('signUp',{
controller:'mainController',
url:'/signUp',
views: {
'main':{
templateUrl: 'templates/signUp.html',
controller: function($scope,$http,$timeout,$state){
$scope.register = function(){
$scope.serverMsg =false;
$http.post('/signUp', $scope.newUser).then(function(data){
$scope.serverMsg = data.data;
if($scope.serverMsg.success == true){
$timeout(function(){
$state.go('login');
}, 2000);
}
})
}
}
}
}
})
.state('login',{
controller:'mainController',
url:'/login',
views: {
'main':{
templateUrl: 'templates/login.html',
controller: function($scope,$http,$timeout,$state,$window){
$scope.setCurrentUser = function(user){
$scope.currentUser = user;
console.log($scope.currentUser);
}
$scope.serverMsg = false;
$http.get('http://localhost:8080/users').then(function({ data: users }) {
$scope.users = users;
})
}
}
}
})
.state('checkIn',{
controller:'mainController',
url:'/checkIn',
views: {
'main':{
templateUrl: 'templates/checkIn.html',
// resolve: {
// },
controller: function($scope,$http,$state,$window,$timeout){
console.log(demo);
var today = new Date();
var hour = today.getHours();
var minute = today.getMinutes();
var meridianPlaceHolder = today.getHours();
meridianPlaceHolder = AmPm(meridianPlaceHolder);
minute = checkTime(minute);
hour = checkHour(hour);
//Handles Times
function AmPm(i){
if(i < 12){
return "AM";
}
else{
return "PM";
}
}
function checkHour(i){
if (i > 12){
i = i - 12;
};
return i;
}
function checkTime(i) {
if (i < 10) {
i = "0" + i
}; // add zero in front of numbers < 10
return i;
}
//var arrivalTime = document.getElementById("arrivalTime");
//Sets a default input value for the arrivalTime
$scope.arrivalTime = hour + ":" + minute + " " + meridianPlaceHolder;
//arrivalTime.setAttribute("value", hour + ":" + minute + " " + meridianPlaceHolder);
//Checks whether or not the user has chosen a latop
var laptopQuery = false;
$('#yesLaptop').click(function(){
laptopQuery = true
});
$('#noLaptop').click(function(){
laptopQuery = false
});
//Handles the Inputs of the Check In Page
$scope.submit = function(){
$scope.serverMsg = false;
console.log("Submitting Check In...");
//Assigns a Random Laptop
var laptops = ["laptop2","laptop3","laptop4","laptop5","laptop6","laptop7","laptop8","laptop9","laptop10","laptop11","laptop12","laptop13","laptop14","laptop15","laptop16","laptop17","laptop18","laptop19","laptop20","laptop21","laptop22","laptop23","laptop24"];
var laptop = laptops.splice([Math.floor(Math.random()*laptops.length)], 1);
//Retrieves input values
var timeLogArrival = document.getElementById("arrivalTime").value;
var timeLogDepature = document.getElementById("depatureTime").value;
var reasonForVisit = document.getElementById("reasonForVisit").value;
//Logs data, isn't currently working on monogo
console.log("Time of Arrival: " + timeLogArrival);
console.log("Time of Departure: " + timeLogDepature);
console.log("Reason for Visit: " + reasonForVisit);
//Displays whether or not a user checke out a laptop or not
if (laptopQuery){
console.log("Your Laptop: " + laptop);
alert("Your Laptop: " + laptop);
}
else{
console.log("You didn't check out a laptop");
}
// var laptopUpdateQuery = {laptopId:laptop};
// var userQuery = {'name':$scope.currentUser.name};
// user.findOneandUpdate(userQuery,laptopUpdateQuery,function(err,doc){
// err ? console.log(err) : alert("success");
// })
$http.put('/check',$scope.currentUser).then(function(data){
$scope.serverMsg = data.data;
if($scope.serverMsg.success == true){
$timeout(function(){
console.log("user data updated");
$state.go('home');
}, 2000);
}
});
}
}
}
}
});
});
// this is for the phone number
app.directive('phoneInput', function($filter, $browser) {
return {
require: 'ngModel',
link: function($scope, $element, $attrs, ngModelCtrl) {
var listener = function() {
var value = $element.val().replace(/[^0-9]/g, '');
$element.val($filter('tel')(value, false));
};
// This runs when we update the text field
ngModelCtrl.$parsers.push(function(viewValue) {
return viewValue.replace(/[^0-9]/g, '').slice(0,10);
});
// This runs when the model gets updated on the scope directly and keeps our view in sync
ngModelCtrl.$render = function() {
$element.val($filter('tel')(ngModelCtrl.$viewValue, false));
};
$element.bind('change', listener);
$element.bind('keydown', function(event) {
var key = event.keyCode;
// If the keys include the CTRL, SHIFT, ALT, or META keys, or the arrow keys, do nothing.
// This lets us support copy and paste too
if (key == 91 || (15 < key && key < 19) || (37 <= key && key <= 40)){
return;
}
$browser.defer(listener); // Have to do this or changes don't get picked up properly
});
$element.bind('paste cut', function() {
$browser.defer(listener);
});
}
};
});
app.filter('tel', function () {
return function (tel) {
console.log(tel);
if (!tel) { return ''; }
var value = tel.toString().trim().replace(/^\+/, '');
if (value.match(/[^0-9]/)) {
return tel;
}
var country, city, number;
switch (value.length) {
case 1:
case 2:
case 3:
city = value;
break;
default:
city = value.slice(0, 3);
number = value.slice(3);
}
if(number){
if(number.length>3){
number = number.slice(0, 3) + '-' + number.slice(3,7);
}
else{
number = number;
}
return ("(" + city + ") " + number).trim();
}
else{
return "(" + city;
}
};
});
//Javascript for the Date to show in the input field by default
// Time
// controller: function($scope,$http,$timeout,$state,$window){
// $scope.login = function(){
// $scope.serverMsg =false;
// $http.get('mongodb://localhost:27017/cubeData', $scope.user.name).then(function(data){
// $scope.serverMsg = data.data;
// if($scope.serverMsg.success == true){
// $timeout(function(){
// $state.go('checkIn');
// }, 2000);
// }
// });
// }
// }
// }
// }
// })
// $scope.loggingOut = false;
// $scope.logout = function(){
// $window.localStorage.removeItem('token');
// $scope.loggingOut = true;
// $timeout(function () {
// $state.go('signUp');
// }, 3000);
// }
There are a few ways to do this, in descending order of correctness.
First - Create a factory/service/value/constant within the app to hold this shared value and include it everywhere you need to access the value, preferably with a getter/setter for the value.
In the following example, it would be preferable to get the values by including myService and calling myService.getMyObject(), but still
possible to simply use myService.myValue e.g. -
app.service('myService', function() {
var service = {
myValue: 'some value',
myObject: { foo: 'bar' },
getMyObject: function() { return service.myObject; },
setMyObject: function(data) { service.myObject = data; }
};
return service;
});
Second - (This is kind of a hack and not a good practice) is to store the value on the $rootScope. This makes it globally available - in controllers, services, and templates. It's easy to pollute using this method, so option 1 is preferred.
Third - (not recommended) You could do it the way you are trying, but it's not advisable. You'd have to either use the controllerAs syntax defining your controllers and access the value through $scope.main.demo, or without controllerAs, through $scope.$parent.demo <-- this gets ugly and confusing fast, depending on nesting.
Create a service to share data across your application
As #LouisK pointed out, the first way it's the suggested one. I would say also that you can consider the angular value too if you want to share just one single variable, otherwise services are the right way.
app.value('myVar', 'Default Value');
In order to change it:
$provide.decorator('myVar', function ($delegate) { return $delegate + ' and same value more'; });
Then simply inject myVar where you need.

Pull to refresh data duplication

I am creating an Ionic application that is pulling articles from a joomla K2 website. I am using $http and just ending my url off with '?format=json' and that is working perfectly. However the website I am pulling data from updates its articles every few minutes so I need a way for the user to be able to refresh the page. I have implemented Ionics pull to refresh and it is working swell except for the fact that instead of just pulling in new articles it just appends all the articles to my array. Is there anyway to just maybe iterate over current articles timestamps or IDs (I am caching articles in localStorage) to just bring in new articles? My factory looks like this:
.factory('Articles', function ($http) {
var articles = [];
storageKey = "articles";
function _getCache() {
var cache = localStorage.getItem(storageKey );
if (cache)
articles = angular.fromJson(cache);
}
return {
all: function () {
return $http.get("http://jsonp.afeld.me/?url=http://mexamplesite.com/index.php?format=json").then(function (response) {
articles = response.data.items;
console.log(response.data.items);
return articles;
});
},
getNew: function () {
return $http.get("http://jsonp.afeld.me/?url=http://mexamplesite.com/index.php?format=json").then(function (response) {
articles = response.data.items;
return articles;
});
},
get: function (articleId) {
if (!articles.length)
_getCache();
for (var i = 0; i < articles.length; i++) {
if (parseInt(articles[i].id) === parseInt(articleId)) {
return articles[i];
}
}
return null;
}
}
});
and my controller:
.controller('GautengCtrl', function ($scope, $stateParams, $timeout, Articles) {
$scope.articles = [];
Articles.all().then(function(data){
$scope.articles = data;
window.localStorage.setItem("articles", JSON.stringify(data));
},
function(err) {
if(window.localStorage.getItem("articles") !== undefined) {
$scope.articles = JSON.parse(window.localStorage.getItem("articles"));
}
}
);
$scope.doRefresh = function() {
Articles.getNew().then(function(articles){
$scope.articles = articles.concat($scope.articles);
$scope.$broadcast('scroll.refreshComplete');
});
};
})
Use underscore.js to simple filtering functionality.
For example:
get all id's of already loaded items (I believe there is some unique fields like id)
http://underscorejs.org/#pluck
var loadedIds = _.pluck($scope.articles, 'id');
Reject all items, if item.id is already in loadedIds list.
http://underscorejs.org/#reject
http://underscorejs.org/#contains
var newItems = _.reject(articles, function(item){
return _.contains(loadedIds, item.id);
});
Join new items and existings:
$scope.articles = newItems.concat($scope.articles);
or
http://underscorejs.org/#union
$scope.articles = _.union(newItems, $scope.articles);
Actually _.union() can manage and remove duplicates, but I would go for manual filtering using item.id.

How can I check whether array of objects is empty or not?

var app = angular.module('app',['ui.bootstrap']);
app.controller("ListCtrl", function ($scope, $http) {
$scope.submit = function () {
$scope.loading = true;
$scope.error = false;
$http.get('http://www.omdbapi.com/?s=' + $scope.search + '&r=json')
.then(function (res) {
var titles = [];
angular.forEach(res.data.Search, function(item){
$http.get('http://www.omdbapi.com/?t=' + item.Title + '&y=&plot=full&r=json').then(function(res){
if (res.data.Poster === "N/A") {
res.data.Poster = "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!";
}
titles.push(res.data);
});
});
$scope.movie = titles;
$scope.results = true;
$scope.error = false;
$scope.loading = false;
if (titles.length==0) { // not working
$scope.results = false;
$scope.error = true;
}
})
I have been tried several things like :
Object.getOwnPropertyNames(titles).length === 0)
obj == null
None of them seems to work...
This is happening because of incorrect scope:
var titles = []; is defined inside the .then
and you are checking the length outside of .then
since titles is not available outside .then it would not work. (undefined.length==0)
Solution:
.then(function (res) {
var titles = [];
angular.forEach(res.data.Search, function(item){
$http.get('http://www.omdbapi.com/?t=' + item.Title + '&y=&plot=full&r=json').then(function(res){
if (res.data.Poster === "N/A") {
res.data.Poster = "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!";
}
titles.push(res.data);
});
$scope.movie = titles;
$scope.results = true;
$scope.error = false;
$scope.loading = false;
if (titles.length==0) { // now this will work
$scope.results = false;
$scope.error = true;
}
});//titles will not be available after this.
$http.get() is async so the statement if (titles.length==0) { gets executed right away.
Have a counter to determine when all the Promises get resolved and then perform the check. Move the if statement inside the callback.
var count = res.data.Search.length;
angular.forEach(res.data.Search, function(item){
$http.get('http://www.o....rest of code').then(function(res) {
// rest of code
titles.push(res.data);
if (!count-- && !titles.length) {
$scope.results = false;
$scope.error = true;
}
}
});
});
In your case, the check
titles.length
will be executed before the
titles.push
because you use an asynchronous request which will return later.
You need to wrap your statements into the answer-block of the request.
Just as an aside, but beneficial for my practice and your future help:
Part of the problem you were having was scope-management (JS-scope, not Angular $scope), part of it was concurrency-management, and part of it appeared to be plain old scope-formatting making it hard to see where all control blocks start and end (that gets miserable when it's not just if/else, but with callbacks/promises, too).
This is a small example of how you might consider tackling these problems, through a quick refactor of your issues:
function pluck (key) {
return function pluckFrom(obj) { return obj[key]; };
}
angular.module("app", ["ui.bootstrap"]);
angular.moule("app").service("omdbService", ["$http", function ($http) {
function getSearch (search) {
var searching = $http.get("http://www.omdbapi.com/?s=" + search + "&r=json")
.then(pluck("data"));
return searching;
}
function getMovie (title) {
var searching = $http.get("http://www.omdbapi.com/?t=" + title + "&y=&plot=full&r=json")
.then(pluck("data"));
return searching;
}
return {
getSearch: getSearch,
getMovie: getMovie,
getPlaceholderPoster: function () { return "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!"; }
};
}]);
angular.moule("app").controller("ListCtrl", ["$scope", "$q", "omdbService", function ($scope, $q, omdb) {
function loadMovie (movie) {
return omdb.getMovie(movie.Title)["catch"](function () { return undefined; });
}
function movieExists (movie) { return !!movie; }
function updatePoster (movie) {
movie.Poster = movie.Poster || omdb.getPlaceholderPoster();
return movie;
}
function setResults (movies) {
$scope.movie = movies; // $scope.movies, instead?
$scope.results = true;
$scope.error = false;
$scope.loading = false;
}
function handleError () {
$scope.results = false;
$scope.error = true;
}
$scope.submit = function () {
$scope.loading = true;
$scope.error = false;
omdb.getSearch($scope.search)
.then(pluck("Search"))
.then(function (movies) { return $q.all(movies.map(loadMovie)); })
.then(function (movies) { return movies.filter(movieExists).map(updatePoster); })
.then(setResults, handleError);
};
}]);
There are 8000 valid ways of handling this, and everyone will see it a little differently.
This is also not really how I'd tackle it in production, but not too far off...
Moving all endpoint-calls out to a service which is responsible for those means that any controller in your system (with that module as a dependency) can access them.
Doing small things per function and letting Array.prototype methods do the iterating (IE8 can be shimmed if needed) means that each function is super-specific.
By wrapping the controller/service functions in arrays, and naming their dependencies, they're now minification friendly.
The body of submit() is less than 10 lines, and deals with all kinds of crazy async stuff, but I know that I've handled errors like one of the movies returning a 404 (my code should still fire, with the remaining movies, the code of others might not -- most code would either never trigger success or would fail all the way through the program, if the server threw an error for a movie).
Now, I'm not checking that the server is sending the right kind of data for a "movie", but that's different.

AngularJS - Select, set default values for Edit/New

First project working with AngularJS and I am a bit stuck using the select list to either set the default value to the first option for a new, or if its an edit select the value.
I have a form with two select lists. Note, I am thinking i'm wrong in my ng-options tag.
invite.tpl.html
<select ng-model="selectedUser" ng-options="user.id as user.user_name for user in users"></select>
<select ng-model="selectedEvent" ng-options="event.id as event.name for event in events"></select>
A controller that gets/posts JSON.
invite.js
.controller('InviteCtrl', function InviteController( $scope, InviteRes, $state, $stateParams ) {
$scope.inviteId = parseInt($stateParams.inviteId, 10);
$scope.users = InviteRes.Users.query();
$scope.events = InviteRes.Events.query();
//EDIT (HAVE ID) - SET SELECTS TO THE USER/EVENT
if ($scope.inviteId) {
$scope.invite = InviteRes.Invites.get({id: $scope.inviteId});
$scope.selectedUser = $scope.invite.user_id;
$scope.selectedEvent = $scope.invite.event_id;
}
//NEW (NO ID) - SET DEFAULT OPTIONS TO FIRST USER/EVENT
else {
$scope.selectedUser = $scope.users[0];
$scope.selectedEvent = $scope.events[0];
$scope.invite = new InviteRes.Invites();
}
Function to save.
$scope.submit = function() {
$scope.invite.user_id = $scope.selectedUser;
$scope.invite.event_id = $scope.selectedEvent;
//IF ID - UPDATE ELSE NEW
if ($scope.inviteId) {
$scope.invite.$update(function(response) {
$state.transitionTo('invites');
}, function(error) {
$scope.error = error.data;
});
}
else {
$scope.invite.$save(function(response) {
$state.transitionTo('invites');
}, function(error) {
$scope.error = error.data;
});
}
};
And a getting those resources
.factory( 'InviteRes', function ( $resource ) {
return {
Invites: $resource("../invites/:id.json", {id:'#id'}, {'update': {method:'PUT'}, 'remove': {method: 'DELETE', headers: {'Content-Type': 'application/json'}}}),
Users: $resource('../users.json'),
Events: $resource('../events.json'),
};
})
I looked around and found some articles explaining how to do this, but everything I've tried has either given me issues with either setting the values, or saving the form.
The resource API doesn't return immediately - see the docs for the following statement:
It is important to realize that invoking a $resource object method
immediately returns an empty reference
Could it simply be that you're trying to assign the value before it's available?
Could you change your code to read something like:
if ($scope.inviteId) {
$scope.invite = InviteRes.Invites.get({id: $scope.inviteId}, function() {
$scope.selectedUser = $scope.invite.user_id;
$scope.selectedEvent = $scope.invite.event_id;
});
}
In terms of the select directive, I tend to use objects rather than values, e.g.
<select ng-model="selectedUser" ng-options="user.user_name for user in users"></select>
// in controller:
$scope.selectedUser = $scope.users[1];

Angularjs must refresh page to see changes

What I have is simple CRUD operation. Items are listed on page, when user clicks button add, modal pops up, user enters data, and data is saved and should automatically (without refresh)be added to the list on page.
Service:
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).fail(getFailed);
},
addExerciseAndCategories: function(data, initialValues) {
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function(item) {
manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
});
saveChanges().fail(addFailed);
function addFailed() {
removeItem(items, item);
}
},
Controller:
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.queryItems = adminCrudService.querySucceeded(data);
var exerciseIds = _($scope.queryItems).pluck('ExerciseId').uniq().valueOf();
$scope.exerciseAndCategories = [];
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise : exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where($scope.queryItems, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
$scope.items.push(newItem);
});
$scope.$apply();
}
Here is how I add new data from controller:
adminCrudService.addExerciseAndCategories($scope.selectedCategories, { Name: $scope.NewName, Description: $scope.NewDesc });
So my question is, why list isn't updated in real time (when I hit save I must refresh page).
EDIT
Here is my querySuceeded
querySucceeded: function (data) {
items = [];
data.results.forEach(function(item) {
items.push(item);
});
return items;
}
EDIT 2
I believe I've narrowed my problem !
So PW Kad lost two hours with me trying to help me to fix this thing (ad I thank him very very very much for that), but unfortunately with no success. We mostly tried to fix my service, so when I returned to my PC, I've again tried to fix it. I believe my service is fine. (I've made some changes as Kad suggested in his answer).
I believe problem is in controller, I've logged $scope.items, and when I add new item they don't change, after that I've logged $scope.queryItems, and I've noticed that they change after adding new item (without refresh ofc.). So probably problem will be solved by somehow $watching $scope.queryItems after loading initial data, but at the moment I'm not quite sure how to do this.
Alright, I am going to post an answer that should guide you on how to tackle your issue. The issue does not appear to be with Breeze, nor with Angular, but the manner in which you have married the two up. I say this because it is important to understand what you are doing in order to understand the debug process.
Creating an entity adds it to the cache with an entityState of isAdded - that is a true statement, don't think otherwise.
Now for your code...
You don't have to chain your query execution with a promise, but in your case you are returning the data to your controller, and then passing it right back into some function in your service, which wasn't listed in your question. I added a function to replicate what yours probably looks like.
getAllIncluding: function(controllerAction, including) {
var query = breeze.EntityQuery.from(controllerAction).expand(including);
return manager.executeQuery(query).then(querySucceeded).fail(getFailed);
function querySucceeded(data) {
return data.results;
}
},
Now in your controller simply handle the results -
$scope.getAllExercisesAndCategories = function() {
adminCrudService.getAllIncluding("ExercisesAndCategories", "Exercise,ExerciseCategory")
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
// Set your object directly to the data.results, because that is what we are returning from the service
$scope.queryItems = data;
$scope.exerciseAndCategories = [];
Last, let's add the properties we create the entity and see if that gives Angular a chance to bind up properly -
_.forEach(data, function(item) {
var e = manager.createEntity("ExerciseAndCategory");
e.Exercise = addedExercise; e.Category: item.Category;
});
So I've managed to solve my problem ! Not sure if this is right solution but it works now.
I've moved everything to my service, which now looks like this:
function addCategoriesToExercise(tempdata) {
var dataToReturn = [];
var exerciseIds = _(tempdata).pluck('ExerciseId').uniq().valueOf();
var createItem = function (id, exercise) {
return {
ExerciseId: id,
Exercise: exercise,
ExerciseCategories: []
};
};
// cycle through ids
_.forEach(exerciseIds, function (id) {
// get all the queryItems that match
var temp = _.where(tempdata, {
'ExerciseId': id
});
// go to the next if nothing was found.
if (!temp.length) return;
// create a new (clean) item
var newItem = createItem(temp[0].ExerciseId, temp[0].Exercise);
// loop through the queryItems that matched
_.forEach(temp, function (i) {
// if the category has not been added , add it.
if (_.indexOf(newItem.ExerciseCategories, i.ExerciseCategory) < 0) {
newItem.ExerciseCategories.push(i.ExerciseCategory);
}
});
// Add the item to the collection
dataToReturn.push(newItem);
});
return dataToReturn;
}
addExerciseAndCategories: function (data, initialValues) {
newItems = [];
var addedExercise = manager.createEntity("Exercise", initialValues);
_.forEach(data, function (item) {
var entity = manager.createEntity("ExerciseAndCategory", { ExerciseId: addedExercise._backingStore.ExerciseId, CategoryId: item.CategoryId });
items.push(entity);
newItems.push(entity);
});
saveChanges().fail(addFailed);
var itemsToAdd = addCategoriesToExercise(newItems);
_.forEach(itemsToAdd, function (item) {
exerciseAndCategories.push(item);
});
function addFailed() {
removeItem(items, item);
}
}
getAllExercisesAndCategories: function () {
var query = breeze.EntityQuery.from("ExercisesAndCategories").expand("Exercise,ExerciseCategory");
return manager.executeQuery(query).then(getSuceeded).fail(getFailed);
},
function getSuceeded(data) {
items = [];
data.results.forEach(function (item) {
items.push(item);
});
exerciseAndCategories = addCategoriesToExercise(items);
return exerciseAndCategories;
}
And in controller I have only this:
$scope.getAllExercisesAndCategories = function () {
adminExerciseService.getAllExercisesAndCategories()
.then(querySucceeded)
.fail(queryFailed);
};
function querySucceeded(data) {
$scope.items = data;
$scope.$apply();
}

Categories