Angular scope changes in service not affecting template - javascript

I've set up a service that can display a status message on a template.
.service('displayStatus', function ()
{
var statusTime = 5000;
var self = this;
this.show = function ($scope, type, msg)
{
$scope.status = {
type: type,
msg: msg
}
self.timer = setTimeout(function ()
{
self.hide($scope);
}, statusTime);
}
this.hide = function ($scope)
{
$scope.status = {
type: null,
msg: null
}
console.log('hid it', $scope);
}
})
Whenever I want an error to show up, I just call displayStatus.show($scope, 'error', 'Uh oh! An error!'). It's the setTimeout that's giving me problems. While the template will update based on the changes I make in "this.show", after waiting 5 seconds and attempting to hide it, the changes don't apply, even though the console.log shows I'm altering $scope.
Why aren't my changes showing?

You need to wrap the body of your event handler in $scope.$apply, or better yet, use the $timeout service to do your timeouts, which does this for you. After calling your function, $apply triggers a $digest cycle, which is how angular detects changes to the model.
.service('displayStatus', function ($timeout)
{
var statusTime = 5000;
var self = this;
this.show = function ($scope, type, msg)
{
$scope.status = {
type: type,
msg: msg
}
self.timer = $timeout(function ()
{
self.hide($scope);
}, statusTime);
}
this.hide = function ($scope)
{
$scope.status = {
type: null,
msg: null
}
console.log('hid it', $scope);
}
})

Related

Undefined scope variable in AngularJS app

I am trying to set a Boolean property on an element in my array object, which I have in my scope.
In the code given below, when I try to set tasks[id].deleted = true, I get the following error.
angular.js:12798 TypeError: Cannot set property 'deleted' of undefined
at Scope.$scope.delete (main.js:54)
Where am I going wrong?
My whole code file is:
angular.module('ngMaterialTaskListApp')
.controller('MainCtrl', function ($scope, $mdDialog, TaskService) {
// Model from which View populates data
$scope.tasks = [];
console.log($scope.tasks);
$scope.showAddDialog = function (ev) {
$mdDialog.show({
controller: DialogController,
templateUrl: '../views/add-dialog-template.html',
parent: angular.element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: true, //Only for xs and sm screen sizes
locals: { //For DialogController, as tasks
tasks: $scope.tasks
}
});
};
/*----------- Function to delete items onClick of delete icon -----------*/
$scope.delete = function (id) {
console.log($scope.tasks[id]);
console.log(id);
// console.log($scope.tasks[id].name);
$scope.tasks[id].deleted = true;
};
/*----------- DialogController function -----------*/
function DialogController($scope, $mdDialog, tasks) {
$scope.task = {};
$scope.hide = function () {
$mdDialog.hide();
//TODO Add a message as to what happened
};
$scope.cancel = function () {
$mdDialog.cancel();
//TODO Add a message as to what happened
};
/*----------- Method show the add dialog -----------*/
$scope.addData = function () {
if (null !== $scope.task.name && null !== $scope.task.description) {
/*----------- Using moment.js to parse date and time -----------*/
$scope.task.date = moment($scope.task.date, '').format('DD MMM YYYY');
$scope.task.time = moment($scope.task.time, '').format('h:mm a');
$scope.task.done = false; // Every new task is pending!
$scope.task.deleted = false; // Every new task exists!
var GlobalID = Date.now();
console.log(GlobalID);
$scope.task.id = GlobalID;
/*----------- Performing http POST -----------*/
TaskService.postTask($scope.task);
/*----------- Pushing to tasks object in $scope of MainCtrl -----------*/
// Have to update tasks again
tasks.push($scope.task);
$scope.hide();
console.log(tasks); //DEBUGGING
} else {
//TODO ADD INVALID/NULL DATA WARNING
}
};
};
// DEPRECATED - USED FOR DATA WHEN SERVER NOT AVAILABLE
TaskService.getTasks().then(function (response) {
$scope.tasks = response.data.tasks;
}, function (error) {
console.log(error + "This");
});
//USING THIS TO GET DATA FROM SERVER
TaskService.getAllTasks().then(function (response) {
// console.log(response.data);
$scope.tasks = response.data;
console.log($scope.tasks);
});
});
How is your html? I bet is like this inside a button in ng-repeat:
ng-click="delete(task.id)"
Try putting like this:
ng-click="delete($index)"

undefined $scope variable inside function in jasmine test

I recently started to learn unit test for angular apps. And already faced up with problem. I can not take scope variable from inside executed function. Here is my factory code
angular.module('app').factory('AuthenticationService', AuthenticationService);
AuthenticationService.$inject = ['$http'];
function AuthenticationService($http) {
var service = {};
service.login = login;
return service;
function login(data, callback) {
$http({
method: 'POST',
url: CONFIG.getUrl('auth/login'),
data: data
}).then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
Part of my controller file. I only yet wan to test login function
function AuthCtrl($scope, $location, AuthenticationService) {
var vm = this;
vm.login = login;
vm.dataLogin = {
user_id: '',
password: '',
};
function login() {
vm.dataLoading = true;
AuthenticationService.login(vm.dataLogin, function (response) {
if (response.status == 200) {
if (response.data.error_code == 'auth.credentials.invalid') {
vm.invalidCredentials = true;
} else {
vm.invalidCredentials = false;
if (response.data.session_state == 'otp_required') {
vm.userNumber = response.data.user_phone;
$localStorage['session_token'] = response.data.session_token;
vm.needForm = 'someForm';
} else {
AuthenticationService.setCredentials(response.data);
$state.go('dashboard');
}
vm.dataLoading = false;
}
}
});
}
}
});
And my spec.js
describe('AuthCtrl, ', function() {
var $scope, ctrl;
var authSrvMock;
var mockJson = {
user_id: '001',
session_token: 'some_token'
};
var mockLoginData = {
user_id: '0000102',
password: '123456'
};
var mockResponseData = {
data: {
"session_expires": 1453822506,
"session_state": "otp_required",
"session_token": "tokennnn",
"status": "success",
"user_id": "0000102",
"user_phone": "+7 (XXX) XXX-XX-89"
},
status: 200
};
beforeEach(function () {
authSrvMock = jasmine.createSpyObj('AuthenticationService', ['login', 'logout']);
module('app');
inject(function ($rootScope, $controller, $q) {
$scope = $rootScope.$new();
authSrvMock.login.and.returnValue(mockResponseData);
ctrl = $controller('AuthCtrl', {
$scope: $scope,
AuthenticationService: authSrvMock
});
});
});
it('should call login function and pass to dashboard', function () {
ctrl.login();
expect(authSrvMock.login).toHaveBeenCalled();
// until this everything works here just fine
});
});
But after I want to test vm.invalidCredentials, if I will write
expect(ctrl.invalidCredentials).toBe(false)
I will get the error
Expected undefined to be false.
Why I can't see variables?
Bit of a noob myself at Jasmine, but I'm guessing it's because you need to get the promise from your login() to return in Jasmine.
Look into using $q.defer(), or even $httpBackend.
After some more digging process and experiments I found solution.
Here what I did
(function () {
'use strict';
describe('AuthCtrl', function () {
var controller, scope, myService, q, deferred, ctrl;
var mockResponseData = {
response1: {
//...
},
response2: {
//...
},
response3: {
//...
}
};
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, $q, $httpBackend, AuthenticationService) {
function mockHttp(data, callback) {
deferred = $q.defer();
deferred.promise.then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
controller = $controller;
scope = $rootScope.$new();
myService = AuthenticationService;
q = $q;
myService.login = mockHttp;
}));
describe('when returning promises', function () {
beforeEach(function () {
ctrl = controller('AuthCtrl', {
$scope: scope,
myService: myService
});
ctrl.initController();
});
it('shows another form to validate login process', function () {
ctrl.login();
deferred.resolve(mockResponseData.response1);
scope.$digest();
expect(ctrl.invalidCredentials).toBe(false);
expect(ctrl.needForm).toEqual('2sAuth');
expect(ctrl.dataLoading).toBe(false);
});
});
});
})();
Since in my factory almost every method requires data and callback I've created mockHttp functions which takes those arguments and deferred promise. In it block I simply call need function, resolve promise with my prepared answers mock and check my expectations. Everything work. Thanks to for aiming in wich way to look

AngularJs factory and controller flow of control

I need some help understanding the code below. It is taken from:
http://www.html5rocks.com/en/tutorials/frameworks/angular-websockets
Factory:
app.factory('socket', function ($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
}
};
Controller:
function AppCtrl($scope, socket) {
// Socket listeners
// ================
socket.on('init', function (data) {
$scope.name = data.name;
$scope.users = data.users;
});
$scope.sendMessage = function () {
socket.emit('send:message', {
message: $scope.message
});
// add the message to our model locally
$scope.messages.push({
user: $scope.name,
text: $scope.message
});
// clear message box
$scope.message = '';
};
}
My doubt is:
What is the flow of control once the controller calls socket.on('init',function(data){.....});. In factory when socket.on is called it takes two parameters eventName and callback. What is this callback?
Why are we using $rootScope.apply
What is callback.apply?
1.What is the flow of control once the controller calls socket.on('init',function(data){.....});. In factory when socket.on is called it takes two parameters eventName and callback. What is this callback?
There is nothing special involved in calling socket.on from your controller. Doing that simply calls the on method in the factory, directly.
Once that's clear, it's easy to see that callback is simply the second parameter passed into that function. In the case of this example, it's the function function (data) { $scope.name = data.name; $scope.users = data.users; }
2.Why are we using $rootScope.apply
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
To ensure that anything that happens in the function inside it is picked up in a digest cycle.
3.What is callback.apply?
It's the apply method that's present on any JavaScript function. This is being used here to call the callback with the socket as the this parameter and the event handler's arguments as the arguments.

Jasmine test using spyon with $httpBackend not working

I am trying to write a jasmine test on some javascript using spyon over a method that uses $http. I have mocked this out using $httpBackend and unfortunately the spy doesn't seem to be picking up the fact the method has indeed been called post $http useage. I can see it being called in debug, so unsure why it reports it hasn't been called. I suspect I have a problem with my scope usage ? or order of $httpBackend.flush\verify ?:
Code under test
function FileUploadController($scope, $http, SharedData, uploadViewModel) {
Removed variables for brevity
.....
$scope.pageLoad = function () {
$scope.getPeriods();
if ($scope.uploadViewModel != null && $scope.uploadViewModel.UploadId > 0) {
$scope.rulesApplied = true;
$scope.UploadId = $scope.uploadViewModel.UploadId;
$scope.linkUploadedData();
} else {
$scope.initDataLinkages();
}
}
$scope.initDataLinkages = function () {
$http({ method: "GET", url: "/api/uploadhistory" }).
success(function (data, status) {
$scope.status = status;
$scope.setUploadHistory(data);
}).
error(function (data, status) {
$scope.data = data || "Request failed";
$scope.status = status;
});
}
$scope.setUploadHistory = function (data) {
if ($scope.UploadId > 0) {
$scope.currentUpload = data.filter(function (item) {
return item.UploadId === $scope.UploadId;
})[0];
//Remove the current upload, to prevent scaling the same data!
var filteredData = data.filter(function (item) {
return item.UploadId !== $scope.UploadId;
});
var defaultOption = {
UploadId: -1,
Filename: 'this file',
TableName: null,
DateUploaded: null
};
$scope.UploadHistory = filteredData;
$scope.UploadHistory.splice(0, 0, defaultOption);
$scope.UploadHistoryId = -1;
$scope.UploadTotal = $scope.currentUpload.TotalAmount;
} else {
$scope.UploadHistory = data;
}
}
Test setup
beforeEach(module('TDAnalytics'));
beforeEach(inject(function (_$rootScope_, $controller, _$httpBackend_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
var sharedData = { currentBucket: { ID: 1 } };
controller = $controller('FileUploadController', { $scope: $scope, SharedData: sharedData, uploadViewModel: null });
$httpBackend.when('GET', '/api/Periods').respond(periods);
$httpBackend.when('GET', '/api/uploadhistory').respond(uploadHistory);
$scope.mappingData = {
FieldMappings: [testDescriptionRawDataField, testSupplierRawDataField],
UserFields: [testDescriptionUserField, testSupplierUserField]
};
}));
afterEach(function() {
testDescriptionRawDataField.UserFields = [];
testSupplierRawDataField.UserFields = [];
testTotalRawDataField.UserFields = [];
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
Working test:
it('pageLoad should call linkUploadedData when user has navigated to the page via the Data Upload History and uploadViewModel.UploadId is set', function () {
// Arrange
spyOn($scope, 'linkUploadedData');
$scope.uploadViewModel = {UploadId: 1};
// Act
$scope.pageLoad();
// Assert
expect($scope.rulesApplied).toEqual(true);
expect($scope.linkUploadedData.calls.count()).toEqual(1);
});
Test that doesn't work (but should. returns count-0 but is called)
it('pageLoad should call setUploadHistory when data returned successfully', function () {
// Arrange
spyOn($scope, 'setUploadHistory');
// Act
$scope.initDataLinkages();
// Assert
expect($scope.setUploadHistory.calls.count()).toEqual(1);
});
The issue is you call httpBackend.flush() after the expect, which means success is called after you do your tests. You must flush before the expect statement.
it('pageLoad should call setUploadHistory when data returned successfully',
inject(function ($httpBackend, $rootScope) {
// Arrange
spyOn($scope, 'setUploadHistory');
// Act
$scope.initDataLinkages();
$httpBackend.flush();
$rootScope.$digest()
// Assert
expect($scope.setUploadHistory.calls.count()).toEqual(1);
}));
You may need to remove the flush statement from after your tests, but it probably should not be there anyway because usually it's a core part of testing behaviour and should be before expect statements.

AngularJS Resolving a Promise

I am learning how to use resolve from an example, and applying it on to my Todo script.
Then I realised an issue, that the example is only showing me how to resolve GET call to get me the Todo List when I first visit this route.
However, in the same route same page I have an Add button to POST new todo item, also a Clear button to DELETE completed items.
Looking at my $scope.addTodo = function() { and $scope.clearCompleted = function () { I want to Resolve my TodoList again after the action. How can I do that?
Here is my code. In my code, the initial resolve: { todos: TodosListResl } is working, it hits TodosListResl function and produces the promise. However, I don't know what to do with addTodo and clearComplete when I want to resolve the todo list again.
main.js
var todoApp = angular.module('TodoApp', ['ngResource', 'ui']);
todoApp.value('restTodo', 'api/1/todo/:id');
todoApp.config(function ($locationProvider, $routeProvider) {
$routeProvider.when("/", { templateUrl: "Templates/_TodosList.html",
controller: TodosListCtrl, resolve: { todos: TodosListResl } });
$routeProvider.otherwise({ redirectTo: '/' });
});
//copied from example, works great
function TodoCtrl($scope, $rootScope, $location) {
$scope.alertMessage = "Welcome";
$scope.alertClass = "alert-info hide";
$rootScope.$on("$routeChangeStart", function (event, next, current) {
$scope.alertMessage = "Loading...";
$scope.alertClass = "progress-striped active progress-warning alert-info";
});
$rootScope.$on("$routeChangeSuccess", function (event, current, previous) {
$scope.alertMessage = "OK";
$scope.alertClass = "progress-success alert-success hide";
$scope.newLocation = $location.path();
});
$rootScope.$on("$routeChangeError",
function (event, current, previous, rejection) {
alert("ROUTE CHANGE ERROR: " + rejection);
$scope.alertMessage = "Failed";
$scope.alertClass = "progress-danger alert-error";
});
}
//also copied from example, works great.
function TodosListResl($q, $route, $timeout, $resource, restTodo) {
var deferred = $q.defer();
var successCb = function(resp) {
if(resp.responseStatus.errorCode) {
deferred.reject(resp.responseStatus.message);
} else {
deferred.resolve(resp);
}
};
$resource(restTodo).get({}, successCb);
return deferred.promise;
}
//now, problem is here in addTodo and clearCompleted functions,
//how do I call resolve to refresh my Todo List again?
function TodosListCtrl($scope, $resource, restTodo, todos) {
$scope.src = $resource(restTodo);
$scope.todos = todos;
$scope.totalTodos = ($scope.todos.result) ? $scope.todos.result.length : 0;
$scope.addTodo = function() {
$scope.src.save({ order: $scope.neworder,
content: $scope.newcontent,
done: false });
//successful callback, but how do I 'resolve' it?
};
$scope.clearCompleted = function () {
var arr = [];
_.each($scope.todos.result, function(todo) {
if(todo.done) arr.push(todo.id);
});
if (arr.length > 0) $scope.src.delete({ ids: arr });
//successful callback, but how do I 'resolve' it?
};
}
I think you're missing the point of resolve. The point of resolve is to " delay route change until data is loaded. In your case, you are already on a route, and you want to stay on that route. But, you want to update the todos variable on the successful callback. In this case, you don't want to use resolve. Instead, just do what needs to be done. For example
$scope.addTodo = function() {
$scope.src.save({ order: $scope.neworder,
content: $scope.newcontent,
done: false }, function () {
todos.push({ order: $scope.neworder,
content: $scope.newcontent,
done: false });
});
//successful callback, but how do I 'resolve' it?
};
As a side point, I noticed you're using _ most likely from the Underscore library. You don't need to use another library for that because Angular already has $angular.forEach().

Categories