$scope.$watch does not seem to watch factory variable - angularjs

I'm a beginner to angularjs. In my NFC project, I want to be able to GET from the server data based on a changing patientId.
However, I am not able to see my $watch execute correctly, even though I see that the patientId changes each time I scan a new NFC tag.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService, PatientRecordsService) {
$scope.tag = NfcService.tag;
$scope.patientId = NfcService.patientId
$scope.$watch(function() {
return NfcService.patientId;
}, function() {
console.log("Inside watch");
PatientRecordsService.getPatientRecords(NfcService.patientId)
.then(
function(response) {
$scope.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
}, true);
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter) {
var tag = {};
var patientId = {};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function(){
angular.copy(nfcEvent.tag, tag);
patientId = $filter('decodePayload')(tag.ndefMessage[0]);
});
console.log("PatientId: ", patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
});
});
return {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
});
Not sure what I'm missing here - please enlighten me!
Update
Per rakslice's recommendation, I created an object to hold my data inside the factory, and now the html (with some server side delay) correctly displays the updated values when a new NFC tag is scanned.
var nfc = angular.module('NfcCtrl', ['PatientRecordsService'])
nfc.controller('NfcCtrl', function($scope, NfcService) {
$scope.tagData = NfcService.tagData;
$scope.clear = function() {
NfcService.clearTag();
};
});
nfc.factory('NfcService', function($rootScope, $ionicPlatform, $filter, PatientRecordsServi\
ce) {
var tagData = {
tag: null,
patientId: null,
patientRecords: []
};
$ionicPlatform.ready(function() {
nfc.addNdefListener(function(nfcEvent) {
//console.log(JSON.stringify(nfcEvent.tag, null, 4));
$rootScope.$apply(function() {
tagData.tag = nfcEvent.tag;
tagData.patientId = $filter('decodePayload')(tagData.tag.ndefMessage[0]);
PatientRecordsService.getPatientRecords(tagData.patientId)
.then(
function(response) {
tagData.patientRecords = response
},
function(httpError) {
throw httpError.status + " : " +
httpError.data;
});
});
console.log("Tag: ", tagData.tag);
console.log("PatientId: ", tagData.patientId);
}, function() {
console.log("Listening for NDEF Tags.");
}, function(reason) {
alert("Error adding NFC Listener " + reason);
})
});
return {
tagData: tagData,
clearTag: function() {
angular.copy({}, this.tagData);
}
};
});

Your code doesn't update the patientId value in the returned NfcService, only the local variable patientId inside the factory function.
Try saving a reference to the object you're returning in the factory function as in a local variable and use that to update the patientId.
For instance, change the creation of the object to put it in a local variable:
var nfcService = {
tag: tag,
patientId: patientId,
clearTag: function () {
angular.copy({}, this.tag);
}
};
...
return nfcService
and then change the patientId update to change the value in the object through the variable.
nfcService.patientId = $filter('decodePayload')(tag.ndefMessage[0]);
Update:
The basic fact about JavaScript that you need to understand is that when you assign one variable to another, if the first variable had a primitive data value the second variable gets a copy of that value, so changing the first variable doesn't affect the second variable after that, but if the first variable had an object reference the second variable gets pointed at that same object that the first variable is pointed at, and changing the object in the first variable after that will affect what you see through the second variable, since it's looking at the same object.
A quick experiment in the browser JavaScript console should give you the idea:
> var a = 1;
> a
1
> var b = a;
> b
1
> a = 5;
> a
5
> b
1
vs.
> var a = {foo: 1}
> var b = a
> a.foo = 5
> a.foo
5
> b.foo
5

Related

Redirection error in angularjs

After adding data in location table, clicking on the save button should redirect it to list of data in location table.But ,it stays in the same page after adding.The same path is given to modify location,it works fine. whereas the same path does not redirect when add location.
function locationController($scope, $state, $rootScope, locationServices,$location, locations, location, primaryLocation, $stateParams,locationTypes, countries) {
var vm = this;
$scope.locations = locations.data;
$scope.location = location.data;
if (primaryLocation.data && primaryLocation.data[0])
$scope.primaryLocation = primaryLocation.data[0];
if (!$scope.location) {
var location = {};
if ($stateParams.accountId) {
$scope.location = {accountId: $stateParams.accountId };
} else {
$scope.location = location;
}
}
$rootScope.title = "Locations";
$scope.locationslist = "views/locations.html";
$scope.addOrModifyLocation = function (location) {
if (location._id) {
locationServices.modifyLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
// $state.reload();
})
} else {
location.status = 'ACTIVE';
locationServices.addLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
})
}
};
If you want angular to know about your $location update, you have to do it like this :
$rootScope.$apply(function() {
$location.path("/my-path"); // path must start with leading /
});
If you're using ui-router, a cleaner approach would be to use
$state.go('stateName', {'accountId' : location.accountId, });
edit :
If you have errors that happen during a state change, you can see it by adding the following code in your app after declaring your module :
angular.module("appName").run([
"$rootScope",
function ($rootScope) {
$rootScope.$on("$stateChangeError", function(error) {
console.log(error);
});
}
]);

Testing controller with resolve dependencies

I'm trying to unit test a controller which relies on resolve keys using Jasmine. I am also using the controllerAs syntax. The routing code is as follows:
$routeProvider.when('/questions', {
templateUrl: 'questions/partial/main_question_viewer/main_question_viewer.html',
controller:'MainQuestionViewerCtrl',
controllerAs:'questionCtrl',
resolve: {
default_page_size: ['QuestionService', function (QuestionService) {
//TODO Work out page size for users screen
return 50;
}],
starting_questions: ['QuestionService', function (QuestionService) {
var questions = [];
QuestionService.getQuestions(1).then(
function(response){
questions = response;
}
);
return questions;
}],
},
});
The controller (so far):
angular.module('questions').controller('MainQuestionViewerCtrl',
[
'QuestionService',
'starting_questions',
'default_page_size',
function (QuestionService, starting_questions, default_page_size) {
var self = this;
//Model Definition/Instantiation
self.questions = starting_questions;
self.page_size = default_page_size;
self.filters = [];
//Pagination Getters (state stored by QuestionService)
self.current_page = function(){
return QuestionService.get_pagination_info().current_page_number;
}
self.page_size = function(page_size){
if(page_size != null){
QuestionService.set_page_size(page_size);
}
return QuestionService.get_page_size();
}
}
]
);
And the test code:
describe('MainQuestionViewerCtrl', function () {
//===============================TEST DATA=====================================
var allQuestionsResponsePage1 = {
count: 4,
next: "https://dentest.com/questions/?format=json&page=2&page_size=1",
previous: null,
results: [
{
id: 1,
subtopic: {
topic: "Math",
name: "Algebra"
},
question: "if a=3 and b=4 what is a+b?",
answer: "7",
restricted: false
}
]
};
beforeEach(module('questions'));
beforeEach(module('globalConstants')); //Need REST URL for mocking responses
var ctrl, qService;
var backend,baseURL;
//inject dependencies
beforeEach(inject(function ($controller, $httpBackend,REST_BASE_URL) {
ctrl = $controller('MainQuestionViewerCtrl');
backend = $httpBackend;
baseURL = REST_BASE_URL;
}));
//inject QuestionService and set up spies
beforeEach(inject(function (QuestionService) {
qService = QuestionService;
}));
//Convenience for adding query params to mocked requests
var buildParams = function (page, page_size) {
var params = {
format: 'json',
page: page,
page_size: page_size,
};
var keys = Object.keys(params).sort(); //how angular orders query params
var returnString = '?' + keys[0] + '=' + params[keys[0]] +
'&' + keys[1] + '=' + params[keys[1]] + '&' + keys[2] + '=' + params[keys[2]];
return returnString;
};
describe('Instantiation',inject(function ($controller) {
beforeEach(module($provide){
beforeEach(inject(function ($controller) {
//Make a mock call to the server to set up state for the QuestionService
backend.expectGET(baseURL + '/questions/' + buildParams(1, 1)).respond(200, allQuestionsResponsePage1);
qService.getQuestions(1);
backend.flush();
//Now mock the result of resolve on route
ctrl = $controller('MainQuestionViewerCtrl', {
default_page_size: 1,
starting_questions: allQuestionsResponsePage1,
});
}));
it('should start with the first page of all the questions pulled down', function () {
expect(qService.questions).toEqual(allQuestionsResponsePage1);
});
it('should start on page 1', function () {
expect(qService.current_page).toEqual(1);
});
it('should start with the page size set to the default passed in',function(){
expect(qService.page_size).toEqual(1);
})
}));
When trying to run the tests, Angular is complaining that it cant resolve starting_questions or default_page_size because the providers for them aren't known.
It worth pointing out that the reason for mocking the HTTP request for the QuestionService is that it builds pagination info based on the response, which the controller will then access to determine the paginator size/numbers in the UI.
Solved. I was instantiating the controller in the outer describe without passing in mock values for the resolve key dependecies. That was causing the error: the method of instantiating the controller with the mock dependecies works fine.

Angular.js - Digest is not including $scope member changes

I have a service that includes:
newStatusEvent = function(account, eventId, url, deferred, iteration) {
var checkIteration;
checkIteration = function(data) {
if (iteration < CHECK_ITERATIONS && data.Automation.Status !== 'FAILED') {
iteration++;
$timeout((function() {
return newStatusEvent(account, eventId, url, deferred, iteration);
}), TIME_ITERATION);
} else {
deferred.reject('failure');
}
};
url.get().then(function(data) {
if (data.Automation.Status !== 'COMPLETED') {
checkIteration(data);
} else {
deferred.resolve('complete');
}
});
return deferred.promise;
};
runEventCheck = function(account, eventId, modalInstance, state) {
newStatusEvent(account, eventId, urlBuilder(account, eventId),
$q.defer(), 0)
.then(function() {
scopeMutateSuccess(modalInstance, state);
}, function() {
scopeMutateFailure(modalInstance);
})["finally"](function() {
modalEventConfig.disableButtonsForRun = false;
});
};
var modalEventConfig = {
disableButtonsForRun: false,
statusBar: false,
nodeStatus: 'Building',
statusType: 'warning'
}
function scopeMutateSuccess(modalInstance, state){
/////////////////////////////////////////////////
//THE SCPOPE DATA MEMBERS THAT ARE CHANGED BUT
//CURRENT DIGEST() DOES NOT INCLUDE THE CHANGE
modalEventConfig.statusType = 'success';
modalEventConfig.nodeStatus = 'Completed Successfully';
//////////////////////////////////////////////////
$timeout(function() {
scopeMutateResetValues();
return modalInstance.close();
}, TIME_CLOSE_MODAL);
state.forceReload();
}
modalEventConfig.scopeMutateStart = scopeMutateStart;
modalEventConfig.close = scopeMutateResetValues;
return {
runEventCheck: runEventCheck,
modalEventConfig: modalEventConfig
};
And here is the controller:
angular.module('main.loadbalancer').controller('EditNodeCtrl', function($scope, $modalInstance, Configuration, LoadBalancerService, NodeService, StatusTrackerService, $state, $q) {
NodeService.nodeId = $scope.id;
$q.all([NodeService.getNode(), LoadBalancerService.getLoadBalancer()]).then(function(_arg) {
var lb, node;
node = _arg[0], lb = _arg[1];
$scope.node = node;
return $scope.save = function() {
$scope.modalEventConfig.scopeMutateStart();
return NodeService.updateNode({
account_number: lb.customer,
ip: node.address,
port: node.port_number,
label: node.label,
admin_state: node.admin_state,
comment: node.comment,
health_strategy: {
http_request: "" + node.healthMethod + " " + node.healthUri,
http_response_accept: "200-299"
},
vendor_extensions: {}
}).then(function(eventId) {
return StatusTrackerService.runEventCheck(lb.customer, eventId,
$modalInstance, $state);
});
}
});
$scope.modalEventConfig = StatusTrackerService.modalEventConfig;
The issue I am having is in the service. After a successful resolve in newStatusEvent and scopeMutateSuccess(modalInstance, state); runs... the modalEventConfig.statusType = 'success'; and modalEventConfig.nodeStatus = 'Completed Successfully'; changes aren't reflected in the view.
Normally, this would be because a digest() is needed to make angular.js aware of a change. However, I have verified in the stack(chromium debugger) that a digest() was called earlier in the stack and is still in effect when the scope members are mutated in function scopeMutateSuccess(modalInstance, state);
What is weird, if I add $rootScope.$apply() after modalEventConfig.nodeStatus = 'Completed Successfully';...then Angular.js will complain a digest() is already in progress...BUT...the view will successfully update and reflect the new changes in from the scope members nodeStatus and statusType. But, obviously this is not the answer/appropriate fix.
So, the question is why isn't the digest() that is currently running from the beginning of the stack(stack from chromium debugger) making angular.js aware of the scope changes for modalEventConfig.statusType = 'success' and modalEventConfig.nodeStatus = 'Completed Successfully'? What can I do to fix this?
$scope.modalEventConfig = StatusTrackerService.modalEventConfig; is a synchronous call, you need treat things asynchronously .
You need wait on promise(resolved by service) at calling area also, i.e. in the controller .
Fixed it.
function scopeMutateSuccess(modalInstance, state){
/////////////////////////////////////////////////
//THE SCPOPE DATA MEMBERS THAT ARE CHANGED BUT
//CURRENT DIGEST() DOES NOT INCLUDE THE CHANGE
modalEventConfig.statusType = 'success';
modalEventConfig.nodeStatus = 'Completed Successfully';
//////////////////////////////////////////////////
$timeout(function() {
scopeMutateResetValues();
state.forceReload();
return modalInstance.close();
}, TIME_CLOSE_MODAL);
}
I am using ui-router and I do a refresh with it useing $delegate. I place state.forceReload(); in the $timeout...the scope members update as they should. I have no idea why exactly, but I am glad this painful experience has come to a end.

How Angular "$watchs" arrays but wont watch properties?

I have two controllers communicating via a service (factory).
OfficerCtrl and NotificationCtrl they communicate via the notification service.
Here's the NotificationCtrl and the notification service.
angular.module('transimat.notification', [])
.controller('NotificationsCtrl', function($scope, notification) {
$scope.msgs = notification.getMsgs();
$scope.showNotification = false;
$scope.$watchCollection('msgs', function() {
//put break point here: It enters
$scope.showNotification = $scope.msgs.length > 0;
});
$scope.close = function(index) {
notification.close(index);
};
$scope.showModal = false;
$scope.modalMsg = notification.getModalMsg();
$scope.$watch('modalMsg', function() {
//put break point here: Wont enter
$scope.showModal = $scope.modalMsg !== null;
},
true);
})
.factory('notification', function($interval) {
var severity = ['success','info','warning','danger','default'];
var msgs = [];
var modalMsg = null;
var notification = {
showSuccess: function(summary, detail, delay) {
showMsg(severity[0], summary, detail, delay);
},
//...
close: function(index) {
msgs.splice(index,1);
return msgs;
},
getMsgs: function() {
return msgs;
},
// MODALS
getModalMsg: function() {
return modalMsg;
},
showModalMsg: function(summary, detail, uri) {
modalMsg = {
summary: summary,
detail: detail,
uri: uri
};
},
closeModal: function() {
modalMsg = null;
}
};
var showMsg = function(severity, summary, detail, delay) {
var msg = {
severity: severity,
summary: summary,
detail: detail
};
msgs.push(msg);
if (delay > 0) {
$interval(function() {
msgs.splice(msgs.length - 1, 1);
}, delay, 1);
}
};
return notification;
});
That's my notification ctrl/service.
Now in my OfficerCtrl I "push" notifications via the notification service.
angular.module('transimat.officers', [])
.controller('RegisterOfficerCtrl', function (
$scope,
notification) {
// business logic
// this WONT work
$scope.showModal = function(){
notification.showModalMsg('NOW','BLAH','officer/1234');
}
// this works
$scope.showNotification = function() {
notification.showSuccess('Success', 'Blah blah.', 5000);
};
})
It will watch arrays but wont watch "normal" vars.
So I have to questions:
The showNotification() works, but the showModal() wont work. It has to do with some pointer thing? The Arrays have a "strong" pointer and the normal vars have "weak" pointers and get ignored/lost by the $scope.$watch expression?
How do I solve this?
The first watch watches the array of messages msgs of your controller scope, which is the array returned by the service getMsgs() function. So, each time the content of this array changes, the callback function is called. When showSuccess() is called, a new message is pushed to the array, and the callback is thus called.
The second watch watches the field modalMsg of your controller. So, each time a new value is assigned to $scope.modalMsg, or each time it's not equal to its previous value, the callback function is called. But a new value is never assigned to this variable. It's only assigned once, before the watch is created. The showModalMsg() function of the service assigns a new value to its own, private, modalMsg variable, but doesn't assign any new value to the controller's modalMsg variable, which still references the old notification modalMsg object:
Before showModalMsg():
$scope.modalMsg -----------------> object
^
|
notification modalMsg ---------------|
After showModalMsg():
$scope.modalMsg -----------------> object
notification modalMsg -----------> other object

Angular js not updating dom when expected

I have a fiddle for this, but basically what it's doing it geo-encoding an address that is input into a textbox. After the address is entered and 'enter' is pressed, the dom does not immediately update, but waits for another change to the textbox. How do I get it to update the table right after a submit?
I'm very new to Angular, but I'm learning. I find it interesting, but I have to learn to think differently.
Here is the fiddle and my controller.js
http://jsfiddle.net/fPBAD/
var myApp = angular.module('geo-encode', []);
function FirstAppCtrl($scope, $http) {
$scope.locations = [];
$scope.text = '';
$scope.nextId = 0;
var geo = new google.maps.Geocoder();
$scope.add = function() {
if (this.text) {
geo.geocode(
{ address : this.text,
region: 'no'
}, function(results, status){
var address = results[0].formatted_address;
var latitude = results[0].geometry.location.hb;
var longitude = results[0].geometry.location.ib;
$scope.locations.push({"name":address, id: $scope.nextId++,"coords":{"lat":latitude,"long":longitude}});
});
this.text = '';
}
}
$scope.remove = function(index) {
$scope.locations = $scope.locations.filter(function(location){
return location.id != index;
})
}
}
Your problem is that the geocode function is asynchronous and therefore updates outside of the AngularJS digest cycle. You can fix this by wrapping your callback function in a call to $scope.$apply, which lets AngularJS know to run a digest because stuff has changed:
geo.geocode(
{ address : this.text,
region: 'no'
}, function(results, status) {
$scope.$apply( function () {
var address = results[0].formatted_address;
var latitude = results[0].geometry.location.hb;
var longitude = results[0].geometry.location.ib;
$scope.locations.push({
"name":address, id: $scope.nextId++,
"coords":{"lat":latitude,"long":longitude}
});
});
});

Resources